package com.headcaselabs.lb.client;

import java.io.IOException;
import java.io.Serializable;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.Hashtable;
import java.util.List;
import java.util.Scanner;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import com.headcaselabs.lb.common.Director;
import com.headcaselabs.lb.common.LBQuery;
import com.headcaselabs.lb.common.PipeCallback;
import com.headcaselabs.lb.common.Record;
import com.headcaselabs.lb.common.Server;

/* 
Copyright 2007, HeadCase Humanufacturing, Inc.

	Licensed under the Apache License, Version 2.0 (the "License"); you
	may not use this file except in compliance with the License. You may
	obtain a copy of the License at
	
	   http://www.apache.org/licenses/LICENSE-2.0
	
	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
	implied. See the License for the specific language governing
	permissions and limitations under the License.
*/
/**
 * PowerDNS Pipe
 * Daemon that connects to a Director and answers queries forwarded by PowerDNS.
 * Communicates via RMI and submits itself as a Callback object to the Director.
 * PowerPipe attempts to find the Director via the local registry. 
 * @author Florian Leibert
 */
//TODO: Let Director handle ALL queries.
public class PowerPipe extends UnicastRemoteObject implements PipeCallback,
		Serializable {

	private static final long										serialVersionUID	= -2179627932061826109L;

	public static final short										TYPE							= 0;

	public static final short										QNAME							= 1;

	public static final short										QCLASS						= 2;

	public static final short										QTYPE							= 3;

	public static final short										ID								= 4;

	public static final short										IP								= 5;

	public static final short										ARGUMENTS					= 6;

	public static final String									DELIM							= "\t";

	public static final String									SEP								= "\t";

	public static final String									END								= "END\n";

	public static final String									OK								= "OK\tPowerPipe has been launched\n";

	public static final String									ZONE_FILE					= "/Users/flo/Programming/zones.xml";

	public static final String									FAIL							= "FAIL\n";

	public static final String									HELO							= "HELO\t1";
	public static final String[]		ANY = { "CNAME", "A", "MX", "NS"};
	
	public static final int LOG_DEBUG	= 0;
	public static final int LOG_INFO = 1;
	public static final int LOG_ERROR = 10;

	// QUERY: DATA qname qclass qtype ttl id content
	public static final boolean									LOG								= false;


	private ZoneFileImporter										importer;

	// The more "static" entries that change infrequently
	private static Hashtable<Query, Record>			records;

	private Director														director;
	
	private String cfgFile;

	protected PowerPipe(int port, String cfg) throws RemoteException {
		super();
		importer = new ZoneFileImporter();
		this.cfgFile = cfg;
		// TODO make this dynamic
		records = importer.importStaticZones(cfgFile);
		//authRecords = importer.importAuthZones(cfgFile);
		try {
			Registry registry = LocateRegistry.getRegistry(port);
			director = (Director) registry.lookup("Director");
		} catch (NotBoundException nbe) {

			nbe.printStackTrace();
			System.exit(1);
			// } catch (MalformedURLException mue) {
			// mue.printStackTrace();
		}
	}
	
	/**
	 * Registers this process with a Director
	 * @throws RemoteException
	 */
	public void registerWithDirector() throws RemoteException {
		director.subscribe(this);
		for (Record r : records.values()) {
			if (r.getLoadbalanced()) {
				log("Found a loadbalanced record",LOG_DEBUG);
				director.addGroup(r.getName());
			}
		}
	}
	//TODO: wrap around Log4J & take care of log levels
	public void log(String toLog, int level) {
		//System.out.println(toLog);
		//System.err.println(toLog);
	}

	@SuppressWarnings("static-access")
	public static void main(String[] args) throws IOException {
		int port = Director.PORT;
		String cfgPath = ZONE_FILE;
		HelpFormatter formatter = new HelpFormatter();
		//TODO: allow specifying more launch arguments
		if (args.length>0) {
			Options options = new Options();
			options.addOption(OptionBuilder.withArgName("port").hasArg().withDescription("port of the RMI registry").create("p"));
			options.addOption(OptionBuilder.withArgName("file").hasArg().withDescription("xml file with zone configuration").create("f"));
			options.addOption("help",true,"this message");
			try {
				CommandLineParser parser = new GnuParser();
				CommandLine cmd = parser.parse( options, args);
				if (cmd.hasOption("p")) {
					port = Integer.parseInt(cmd.getOptionValue("p"));
				} 
				if (cmd.hasOption("f")) {
					cfgPath = cmd.getOptionValue("f");
				}
				if (cmd.hasOption("help")) {
					formatter.printHelp("PowerPipe",options);
					System.exit(1);
				}
				
				
			} catch (ParseException pe) {
				 System.err.println( "Invalid parameters.  Reason: " + pe.getMessage() );
				 formatter.printHelp("PowerPipe",options);
				 System.exit(1);
			}
		}
		
		
		Scanner scanner = new Scanner(System.in);
		String line = scanner.nextLine();
		while (!line.equals(HELO)) {
			System.out.print(FAIL);
			System.err
					.println("Protocol error while trying to launch PowerPipe received:"
							+ line);
			//System.exit(1);
		}
		PowerPipe pt = new PowerPipe(port,cfgPath);
		pt.registerWithDirector();
		System.out.print(OK);
		pt.acceptQueries();
	}
	
	

	/**
	 * Daemon loop that accepts queries. Maybe terminated ungracefully. TODO: Add
	 * control mechanism that terminates the loop "remotely"
	 */
	public void acceptQueries() {
		Scanner scanner = new Scanner(System.in);
		String[] query;
		String line;
		while (true) {
			line = scanner.nextLine();
			query = line.split(DELIM);
			if (query.length != ARGUMENTS) {
				log("Received misformatted query:" + line,LOG_INFO);
				output("LOG\tReceived malformed query\n");
				output("LOG\t"+line+"LOG\t\n");
				output(FAIL);
				//outputEnd();
				continue;
			}
			if (LOG) {
				String out = "";
				for (String s : query)
					out = out + " " + s;
				outputLog("LOG\tReceived query: " + out+"\n",LOG_INFO);
			}
			if (query[0].equals("Q")) {
				handleQuery(query[1], query[2], query[3], query[4], query[5]);
				
			} else if (query[0].equals("P")) {
				output("LOG\tReceived a PING"+query[0]);
				output(OK);
				continue;
			} else if (query[0].equals("AXFR")) {
				outputLog("LOG\tReceived AXFR: " + line,LOG_INFO);
				
				//handleAxfr(query[1], query[2], query[3], query[4], query[5]);
			}
			output(END);

		}
	}

	/**
	 * Wrapper to output a string. String should be well-formed and include all
	 * neccessary EOL and whitespace symbols as defined by the PowerDNS backend
	 * protocol.
	 * 
	 * @param string
	 */
	public void output(String string) {
		System.out.print(string);
	}

	/**
	 * Wrapper method to output a END\n to indicate the END of data.
	 * 
	 */
	public void outputEnd() {
		System.out.print(END);
	}
	
	public void outputLog(String log, int level) {
		//TODO:wrap around Log4J
		System.out.print(log);
		//System.out.print(log);
	
		
	}

	/**
	 * Handles a Query.
	 * 
	 * @param qname
	 * @param qclass
	 * @param qtype
	 * @param id
	 * @param ip
	 */
	public void handleQuery(String qname, String qclass, String qtype, String id,
			String ip) {

		if (qtype.equals("ANY")) {
			for(String s : ANY) {
				handleDetails(qname, qclass, s, id, ip);
			}
			if (LOG) {
				outputLog("LOG\t Received ANY QUERY:" + ip + " for : " + qtype + " : "
						+ qname + " : " + qclass + "\n",LOG_DEBUG);
			}
		} else {
			handleDetails(qname, qclass, qtype, id, ip);
		}
	}
	
	
	

	/*
	 * Handles the query details @param qname @param qclass @param qtype @param id
	 * @param ip
	 */
	private void handleDetails(String qname, String qclass, String qtype,
			String id, String ip) {
		Query query = new Query(qtype, qname);
		Record answer = records.get(query);
		if (answer == null) {
			if (LOG) {
				log("No Record found for query:"+query.toString(),LOG_INFO);
			}
			return;
		}
		if (!answer.getLoadbalanced()) {
			output(answer.getPdnsString());
			if (LOG) {
				outputLog("LOG\t Answered to : " + ip + " for : " + qtype + " : " + qname
						+ " : " + qclass + "\n",LOG_DEBUG);
			}
		} else if (answer.getLoadbalanced()) {
			Record lbrec;
			try {
				lbrec = director.resolve(new LBQuery(query.getType(),query.getName(),ip));
				if (lbrec!=null)
					output(lbrec.getPdnsString());
			} catch (RemoteException re) {
				output("LOG\tException while accessing Director!\n");
			}
			if (LOG) {
				outputLog("LOG\t Answered to : " + ip + " for : " + qtype + " : " + qname
						+ " : " + qclass + " with : " + lbrec.getPdnsString(),LOG_DEBUG);
			}
		}
	}
	
	
	/**
	 * Callback method
	 */
	public void updateServers(String lbName, List<Server> servers) throws RemoteException{
		
	}
	
	/**
	 * Callback Method Adds a node 
	 */
	public void addNode(Record r) throws RemoteException {
		Query q;
		q = new Query(r.getType(),r.getName());
		if (records.get(q)!=null) {
			if (!records.get(q).getLoadbalanced()) {
				//Useful for quickly overwriting static NS entries (i.e. when server is down)
				records.get(q).setLoadbalanced(true);
			}
		}
		else
			records.put(q, r);
	}

	public void quit() throws RemoteException {
		output("LOG\tCaught kill signal from Director. Quitting");
		System.exit(0);
	}
}